﻿using System;
using System.Collections;
using System.Collections.Generic;
using MessagingWPF.Infrastructure.Utilities;

/*
 * Originally based on Microsoft Composite WPF (PRISM),
 * but some significant changes were made since that.
 * Copyright (c) Microsoft Corporation.  
 */

namespace MessagingWPF.Infrastructure
{
	/// <summary>
	/// A dictionary managing a one-too-many relationship between key and values.
	/// </summary>
	/// <typeparam name="TKey">The type of the key.</typeparam>
	/// <typeparam name="TValue">The type of the values.</typeparam>
	public class ListDictionary<TKey, TValue> : IDictionary<TKey, List<TValue>>
	{
		private readonly Dictionary<TKey, List<TValue>> _InnerValues =
			new Dictionary<TKey, List<TValue>>();

		/// <summary>
		/// Gets a shallow copy of all values in all lists.
		/// </summary>
		public List<TValue> Values
		{
			get
			{
				var values = new List<TValue>();

				foreach (IEnumerable<TValue> list in _InnerValues.Values)
				{
					values.AddRange(list);
				}

				return values;
			}
		}

		#region IDictionary<TKey,List<TValue>> Members

		/// <summary>
		/// Gets the list of keys in the dictionary.
		/// </summary>
		public ICollection<TKey> Keys
		{
			get { return _InnerValues.Keys; }
		}

		/// <summary>
		/// Gets the number of lists in the dictionary.
		/// </summary>
		public int Count
		{
			get { return _InnerValues.Count; }
		}

		/// <summary>
		/// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.
		/// </summary>
		/// <value></value>
		/// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false.</returns>
		bool ICollection<KeyValuePair<TKey, List<TValue>>>.IsReadOnly
		{
			get { return ((ICollection<KeyValuePair<TKey, List<TValue>>>) _InnerValues).IsReadOnly; }
		}

		/// <summary>
		/// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
		/// </summary>
		/// <value></value>
		/// <returns>An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>.</returns>
		ICollection<List<TValue>> IDictionary<TKey, List<TValue>>.Values
		{
			get { return _InnerValues.Values; }
		}

		/// <summary>
		/// Gets or sets the list associated with the given key. The 
		/// access always succeeds, eventually returning an empty list.
		/// </summary>
		/// <param name="key">The key of the list to access.</param>
		/// <returns>The list associated with the key.</returns>
		/// <exception cref="ArgumentException">
		/// Thrown when the supplied parameter equals the default value
		/// of its type.
		/// </exception>
		public List<TValue> this[TKey key]
		{
			get
			{
				if (_InnerValues.ContainsKey(key) == false)
				{
					_InnerValues.Add(key, new List<TValue>());
				}

				return _InnerValues[key];
			}

			set { _InnerValues[key] = value; }
		}

		/// <summary>
		/// Removes all entries in the dictionary.
		/// </summary>
		public void Clear()
		{
			_InnerValues.Clear();
		}

		/// <summary>
		/// Determines whether the dictionary contains the given key.
		/// </summary>
		/// <param name="key">The key to locate.</param>
		/// <returns>
		/// <c>true</c> if the dictionary contains the given key; <br/>
		/// Otherwise, <c>false.</c>
		/// </returns>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		public bool ContainsKey(TKey key)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			return _InnerValues.ContainsKey(key);
		}

		/// <summary>
		/// Removes a list by key.
		/// </summary>
		/// <param name="key">The key of the list to remove.</param>
		/// <returns>
		/// <c>true</c> when removed; Otherwise <c>false</c>.
		/// </returns>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		public bool Remove(TKey key)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			return _InnerValues.Remove(key);
		}

		/// <summary>
		/// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2"/>.
		/// </summary>
		/// <param name="key">The object to use as the key of the element to add.</param>
		/// <param name="value">The object to use as the value of the element to add.</param>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> or <paramref name="value"/> equals <c>null</c>.
		/// </exception>
		/// <exception cref="T:System.ArgumentException">An element with the same key already exists in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</exception>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only.</exception>
		void IDictionary<TKey, List<TValue>>.Add(TKey key, List<TValue> value)
		{
			Ensure.ArgumentIsNotNull(key, "key");
			Ensure.ArgumentIsNotNull(value, "value");

			_InnerValues.Add(key, value);
		}

		/// <summary>
		/// Gets the value associated with the specified key.
		/// </summary>
		/// <param name="key">The key whose value to get.</param>
		/// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value"/> parameter. This parameter is passed uninitialized.</param>
		/// <returns>
		/// Returns true if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key; otherwise, false.
		/// </returns>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		bool IDictionary<TKey, List<TValue>>.TryGetValue(TKey key, out List<TValue> value)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			return _InnerValues.TryGetValue(key, out value);
		}

		/// <summary>
		/// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>.
		/// </summary>
		/// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
		void ICollection<KeyValuePair<TKey, List<TValue>>>.Add(KeyValuePair<TKey, List<TValue>> item)
		{
			((ICollection<KeyValuePair<TKey, List<TValue>>>) _InnerValues).Add(item);
		}

		/// <summary>
		/// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"/> contains a specific value.
		/// </summary>
		/// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
		/// <returns>
		/// Returns true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false.
		/// </returns>
		bool ICollection<KeyValuePair<TKey, List<TValue>>>.Contains(KeyValuePair<TKey, List<TValue>> item)
		{
			return ((ICollection<KeyValuePair<TKey, List<TValue>>>) _InnerValues).Contains(item);
		}

		/// <summary>
		/// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
		/// </summary>
		/// <param name="array">The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"/>. The <see cref="T:System.Array"/> must have zero-based indexing.</param>
		/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
		/// <exception cref="T:System.ArgumentNullException">
		/// Thrown when	<paramref name="array"/> is null.</exception>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// Thrown when	<paramref name="arrayIndex"/> is less than 0.</exception>
		/// <exception cref="T:System.ArgumentException">
		/// Thrown when	<paramref name="array"/> is multidimensional.-or-<paramref name="arrayIndex"/> is equal to or greater than the length of <paramref name="array"/>.-or-The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"/> is greater than the available space from <paramref name="arrayIndex"/> to the end of the destination <paramref name="array"/>.-or-Type <paramref name="T"/> cannot be cast automatically to the type of the destination <paramref name="array"/>.</exception>
		void ICollection<KeyValuePair<TKey, List<TValue>>>.CopyTo(KeyValuePair<TKey, List<TValue>>[] array, int arrayIndex)
		{
			((ICollection<KeyValuePair<TKey, List<TValue>>>) _InnerValues).CopyTo(array, arrayIndex);
		}

		/// <summary>
		/// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>.
		/// </summary>
		/// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param>
		/// <returns>
		/// Returns true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>.
		/// </returns>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only.</exception>
		bool ICollection<KeyValuePair<TKey, List<TValue>>>.Remove(KeyValuePair<TKey, List<TValue>> item)
		{
			return ((ICollection<KeyValuePair<TKey, List<TValue>>>) _InnerValues).Remove(item);
		}

		/// <summary>
		/// Returns an enumerator that iterates through the collection.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
		/// </returns>
		IEnumerator<KeyValuePair<TKey, List<TValue>>> IEnumerable<KeyValuePair<TKey, List<TValue>>>.GetEnumerator()
		{
			return _InnerValues.GetEnumerator();
		}

		/// <summary>
		/// Returns an enumerator that iterates through a collection.
		/// </summary>
		/// <returns>
		/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
		/// </returns>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return _InnerValues.GetEnumerator();
		}

		#endregion

		/// <summary>
		/// Registers a key.
		/// </summary>
		/// <param name="key">The key of the list that will hold the value.</param>
		/// <remarks>
		/// If a list does not already exist, it will be created automatically.
		/// </remarks>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		public void Add(TKey key)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			CreateNewList(key);
		}

		/// <summary>
		/// Adds a value to a list with the given key. If a list does not already exist, 
		/// it will be created automatically.
		/// </summary>
		/// <param name="key">The key of the list that will hold the value.</param>
		/// <param name="value">The value to add to the list under the given key.</param>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		public void Add(TKey key, TValue value)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			if (_InnerValues.ContainsKey(key))
			{
				_InnerValues[key].Add(value);
			}
			else
			{
				var values = CreateNewList(key);
				values.Add(value);
			}
		}

		/// <summary>
		/// Determines whether the dictionary contains the specified value.
		/// </summary>
		/// <param name="value">The value to locate.</param>
		/// <returns>
		/// <c>true</c> if the dictionary contains the value in any list; <br />
		/// Otherwise, <c>false</c>.</returns>
		public bool ContainsValue(TValue value)
		{
			foreach (var pair in _InnerValues)
			{
				if (pair.Value.Contains(value))
				{
					return true;
				}
			}

			return false;
		}

		/// <summary>
		/// Retrieves the all the elements from the list which have a key that matches the condition 
		/// defined by the specified predicate.
		/// </summary>
		/// <param name="keyFilter">The filter with the condition to use to filter lists by their key.</param>
		/// <returns>
		/// A set of values found by the specified filter.
		/// </returns>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="keyFilter"/> is <c>null</c>.
		/// </exception>
		public IEnumerable<TValue> FindAllValuesByKey(Predicate<TKey> keyFilter)
		{
			Ensure.ArgumentIsNotNull(keyFilter, "keyFilter");

			foreach (var pair in this)
			{
				if (keyFilter(pair.Key))
				{
					foreach (var value in pair.Value)
					{
						yield return value;
					}
				}
			}
		}

		/// <summary>
		/// Retrieves all the elements that match the condition defined by the specified predicate.
		/// </summary>
		/// <param name="valueFilter">The filter with the condition to use to filter values.</param>
		/// <returns>
		/// A set of values found by the specified filter.
		/// </returns>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="valueFilter"/> is <c>null</c>.
		/// </exception>
		public IEnumerable<TValue> FindAllValues(Predicate<TValue> valueFilter)
		{
			Ensure.ArgumentIsNotNull(valueFilter, "valueFilter");

			foreach (var pair in this)
			{
				foreach (var value in pair.Value)
				{
					if (valueFilter(value))
					{
						yield return value;
					}
				}
			}
		}

		/// <summary>
		/// Removes a value from the list with the given key.
		/// </summary>
		/// <param name="key">The key of the list where the value exists.</param>
		/// <param name="value">The value to remove.</param>
		/// <exception cref="ArgumentNullException">
		/// Thrown when <paramref name="key"/> equals <c>null</c>.
		/// </exception>
		public void Remove(TKey key, TValue value)
		{
			Ensure.ArgumentIsNotNull(key, "key");

			if (_InnerValues.ContainsKey(key))
			{
				_InnerValues[key].RemoveAll(item => Equals(value, item));
			}
		}

		/// <summary>
		/// Removes a value from all lists where it may be found.
		/// </summary>
		/// <param name="value">The value to remove.</param>
		public void Remove(TValue value)
		{
			foreach (var pair in _InnerValues)
			{
				Remove(pair.Key, value);
			}
		}

		private List<TValue> CreateNewList(TKey key)
		{
			List<TValue> values;
			if (!_InnerValues.TryGetValue(key, out values))
			{
				values = new List<TValue>();
				_InnerValues.Add(key, values);
			}

			return values;
		}
	}
}